package dataStorage;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import entity.Expense;
import entity.Income;
import entity.XMLentity;
import entityManager.ExpenseManager;
import entityManager.IncomeManager;

import settings.IdManager;

public class DataXML implements DataHandler {

	private entity.Undo[] UndoList = { null, null, null, null, null }; 
	// UndoList[0] is the most recent undo
	private int UndoListLength = 0;
	private boolean isUndoOverride = false;

	private static DataXML instance = null;

	public static DataXML getInstance() {

		if (instance == null) {

			System.out.println("new dataXML created!");
			instance = new DataXML();
		}
		// else System.out.println("old dataXML returned!");

		return instance;
	}

	// @author U096819H
	public ArrayList<XMLentity> load(String key, Class c) throws JAXBException,
			ParserConfigurationException, SAXException, IOException,
			TransformerConfigurationException, TransformerException,
			InstantiationException, IllegalAccessException {
		// instance of DataDirectory
		final DataDirectory fd = new DataDirectory();
		File file = new File(fd.getPathname(), fd.getFilename(key));

		// XML handlers
		final JAXBContext context = JAXBContext.newInstance(c);
		final Unmarshaller unmarshaller = context.createUnmarshaller();
		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
		Document doc = docBuilder.parse(file);

		// load all the nodes of file
		NodeList list = doc.getElementsByTagName(key);
		// return list
		ArrayList<entity.XMLentity> returnList = new ArrayList<entity.XMLentity>();
		for (int i = 0; i < list.getLength(); i++) {
			Object o = c.newInstance();
			o = unmarshaller.unmarshal(list.item(i));
			if (entity.XMLentity.class.isAssignableFrom(o.getClass())) {
				returnList.add((entity.XMLentity) o);
			} else {
				System.out
						.println("when reading from the file, a non-XMLentity object was read of type "
								+ o.getClass());
			}
		}
		return returnList;
	}

	//@author U096819H
	public boolean reset(String key) throws IOException {
		final DataDirectory fd = new DataDirectory();
		File f = new File(fd.getPathname(), fd.getFilename(key));
		// delete first
		f.delete();
		// create the file
		f.createNewFile();
		// update the XML stuff
		String toWrite = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<"
				+ key + "EzXpns>\n</" + key + "EzXpns>";
		FileWriter fstream = new FileWriter(f);
		BufferedWriter out = new BufferedWriter(fstream);
		out.write(toWrite);
		out.close();
		return true;
	}

	//@author U096819H
	public boolean resetAll() {
		return true;
	}

	//@author U096819H
	public boolean update(String id, entity.XMLentity o, String key, Class c)
			throws JAXBException, ParserConfigurationException, SAXException,
			IOException, TransformerConfigurationException,
			TransformerException, InstantiationException,
			IllegalAccessException {
		return this.update(id, o, key, c, false);
	}

	// @author U096819H
	public boolean update(String id, entity.XMLentity o, String key, Class c,
			boolean isUndo) throws JAXBException, ParserConfigurationException,
			SAXException, IOException, TransformerConfigurationException,
			TransformerException, InstantiationException,
			IllegalAccessException {
		if (isUndoOverride)
			isUndo = true;
		if (key == "idObject") {// if it's an idObject don't worry yo'self none
								// about the undo, just use Rajul's code
			// remove from file
			if (remove(o, key)) {
				// write the update object in file
				if (write(o, key, c)) {
					return true;
				} else {
					return false;
				}
			} else {
				return false;
			}
		}
		entity.XMLentity undoObject = null;
		ArrayList<entity.XMLentity> tempArray;
		tempArray = this.load(key, c);
		// this finds the object we are about to update (so we can read its
		// original state
		// System.out.println("attempting to find id " + id +
		// " from an array of size " + tempArray.size());
		for (int i = 0; i < tempArray.size(); i++) {
			// System.out.println("trying " + tempArray.get(i).getId());
			if (tempArray.get(i).getId().equals(id)) {
				undoObject = tempArray.get(i);
				break;
			}
		}
		// System.out.println("hacky method RETURNED:" + undoObject.getId());

		if (key != "idObject") { // update the object (and then add an undo)

			if (key == "category" && isUndo) {// if it is not an undo then the
												// categorymanager will take
												// care of this
				// System.out.println("BEGIN CATEGORY UPDATE");
				// this makes sure anything happening in this function will not
				// generate Undo objects
				isUndoOverride = true;
				ExpenseManager em = new ExpenseManager();
				IncomeManager im = new IncomeManager();
				ArrayList<Expense> aEx = em.getByCategory((entity.Category) o);
				for (int i = 0; i < aEx.size(); i++) {
					aEx.get(i).getCategory()
							.setName(((entity.Category) o).getName());
					em.update(aEx.get(i));
				}
				ArrayList<Income> aIn = im.getByCategory((entity.Category) o);
				for (int i = 0; i < aIn.size(); i++) {
					aIn.get(i).getCategory()
							.setName(((entity.Category) o).getName());
					im.update(aIn.get(i));
				}
				isUndoOverride = false;
				// System.out.println("END CATEGORY UPDATE");
			}
			if (key == "source" && isUndo) {
				// this makes sure anything happening in this function will not
				// generate Undo objects
				isUndoOverride = true;
				ExpenseManager em = new ExpenseManager();
				IncomeManager im = new IncomeManager();
				ArrayList<Expense> aEx = em.getBySource((entity.Source) o);
				for (int i = 0; i < aEx.size(); i++) {
					aEx.get(i).getSource()
							.setName(((entity.Source) o).getName());
					em.update(aEx.get(i));
				}
				ArrayList<Income> aIn = im.getBySource((entity.Source) o);
				for (int i = 0; i < aIn.size(); i++) {
					aIn.get(i).getSource()
							.setName(((entity.Source) o).getName());
					im.update(aIn.get(i));
				}
				isUndoOverride = false;
			}

			if (remove(o, key, true)) {
				// write the update object in file

				if (write(o, key, c, true)) {
					if (!isUndo && undoObject != null)
						this.addUndo(new entity.Undo(2, undoObject, key, c));

					return true;
				} else {
					return false;
				}
			} else {
				return false;
			}
		}
		return false;
	}

	//@author U096819H
	public boolean remove(entity.XMLentity o, String key) throws JAXBException,
			ParserConfigurationException, SAXException, IOException,
			TransformerException, InstantiationException,
			IllegalAccessException {
		return this.remove(o, key, false);
	}

	// @author U096819H
	public boolean remove(entity.XMLentity o, String key, boolean isUndo)
			throws JAXBException, ParserConfigurationException, SAXException,
			IOException, TransformerException, InstantiationException,
			IllegalAccessException {
		// add an undo
		if (isUndoOverride)
			isUndo = true;

		if (key != "idObject" && !isUndo) // We don't want to undo writing to
											// the idObject, or when writing to
											// undo something
			this.addUndo(new entity.Undo(0, o, key, o.getClass()));
		// if (key == "Undo") key = this.tempUndoKey; //By passing "Undo" as the
		// key, we will not add an undo, but what is the real key that we want
		// to undo? this is stored in tempUndo before calling this function

		// instance of DataDirectory
		final DataDirectory fd = new DataDirectory();
		File file = new File(fd.getPathname(), fd.getFilename(key));

		// XML Handlers
		final JAXBContext context = JAXBContext.newInstance(o.getClass());
		final Marshaller marshaller = context.createMarshaller();
		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
		Document doc = docBuilder.parse(file);

		// LOGIC-Start
		// get the first root node
		Node first = doc.getFirstChild();
		// get all the nodes of the root node
		NodeList list = doc.getElementsByTagName(key);

		// parse through every node and delete the one with "id" = id
		for (int i = 0; i < list.getLength(); i++) {
			NamedNodeMap attr = list.item(i).getAttributes();
			Node nodeAttr = attr.getNamedItem("id");
			if (nodeAttr.getTextContent().equalsIgnoreCase(o.getId())) {
				NodeList toRemoveList = list.item(i).getChildNodes();
				for (int j = 0; j < toRemoveList.getLength(); j++) {
					list.item(i).removeChild(toRemoveList.item(j));
				}
				first.removeChild(list.item(i));
			}
		}
		// LOGIC-End

		// update the file
		TransformerFactory transformerFactory = TransformerFactory
				.newInstance();
		Transformer transformer = transformerFactory.newTransformer();
		DOMSource source = new DOMSource(doc);
		StreamResult result = new StreamResult(file);
		transformer.transform(source, result);
		// successful remove
		return true;
	}

	//@author U096819H
	public boolean write(entity.XMLentity o, String key, Class c)
			throws JAXBException, ParserConfigurationException, SAXException,
			IOException, TransformerException, InstantiationException,
			IllegalAccessException {
		return this.write(o, key, c, false);
	}

	// @author U096819H
	public boolean write(entity.XMLentity o, String key, Class c, boolean isUndo)
			throws JAXBException, ParserConfigurationException, SAXException,
			IOException, TransformerException, InstantiationException,
			IllegalAccessException {
		if (isUndoOverride)
			isUndo = true;
		if (!DataXML.getInstance().equals(this))
			System.out
					.println("this is a different dataXML CRITICAL WARNING, parent = "
							+ Thread.currentThread().getStackTrace()[3]
									.getClassName());
		// instance of DataDirectory
		if (key != "idObject" && !isUndo)
			this.addUndo(new entity.Undo(1, o, key, c));
		// if (isUndo) key = this.tempUndoKey;
		final DataDirectory fd = new DataDirectory();
		File file = new File(fd.getPathname(), fd.getFilename(key));

		System.out
				.println("creating new " + c.getName() + " id = " + o.getId());

		// XML Handles
		final JAXBContext context = JAXBContext.newInstance(c);
		final Marshaller marshaller = context.createMarshaller();
		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
		Document doc = docBuilder.parse(file);

		// LOGIC-Start
		Node first = doc.getFirstChild();
		Node n = first.getFirstChild();
		// Marshal the javaObject and write the XML to the stringWriter
		marshaller.marshal(o, first);
		first.appendChild(n);
		// LOGIC-End

		// persist to the file
		TransformerFactory transformerFactory = TransformerFactory
				.newInstance();
		Transformer transformer = transformerFactory.newTransformer();
		DOMSource source = new DOMSource(doc);
		StreamResult result = new StreamResult(file);
		transformer.transform(source, result);
		return true;
	}


	//@author A0105110J
	private boolean addUndo(entity.Undo u)
			throws TransformerConfigurationException, InstantiationException,
			IllegalAccessException, JAXBException,
			ParserConfigurationException, SAXException, IOException,
			TransformerException {
		System.out.println("adding new undo : " + u.getUndoKey() + "id : "
				+ u.getId());
		if (!DataXML.getInstance().equals(this)) {
			System.out
					.println("WARNING: new instance of DataXML (please replace new DataXML() with DataXML.getInstance");
		}
		for (int i = Math.min(4, this.UndoListLength); i > 0; i--) {
			// System.out.println("i : " + i);
			this.UndoList[i] = this.UndoList[i - 1];
		}

		this.UndoList[0] = u;
		this.UndoListLength = Math.min(5, this.UndoListLength + 1);
		// System.out.println("new length =  " + UndoListLength);
		// this.printUndoList();
		return true;
	}

	//@author A0105110J
	private boolean removeUndo() throws TransformerConfigurationException,
			InstantiationException, IllegalAccessException, JAXBException,
			ParserConfigurationException, SAXException, IOException,
			TransformerException {
		if (this.UndoListLength < 1)
			return false;
		// System.out.println("adding new undo : " + u.getUndoKey() +
		// " and is this the real dataXML? " +
		// DataXML.getInstance().equals(this));
		for (int i = 0; i < Math.min(4, this.UndoListLength); i++) {
			// System.out.println("i : " + i);
			this.UndoList[i] = this.UndoList[i + 1];
		}

		// System.out.println("about to add it.. is write? " + u.isWrite());
		this.UndoListLength--;
		this.UndoList[UndoListLength] = null;
		// printUndoList();
		return true;
		// System.out.println("new length =  " + UndoListLength);

	}

//@author A0105110J
	private void printUndoList() throws TransformerConfigurationException,
			InstantiationException, IllegalAccessException, JAXBException,
			ParserConfigurationException, SAXException, IOException,
			TransformerException {
		System.out.println("UndoList, length " + UndoListLength + " :");
		for (int i = 0; i < this.UndoListLength; i++) {
			System.out.println("undo " + i + " id = "
					+ this.UndoList[i].getUndoId() + " write: "
					+ this.UndoList[i].isWrite());

		}
	}
//@author A0105110J
	public void Undo() throws TransformerConfigurationException,
			InstantiationException, IllegalAccessException, JAXBException,
			ParserConfigurationException, SAXException, IOException,
			TransformerException {
		if (this.UndoListLength == 0) {
			System.out.println(" nothing more to undo...undolist.length = "
					+ this.UndoListLength);
			// return 0;
		} else {
			// printUndoList();
			entity.Undo u = this.UndoList[0];
			// System.out.println("removing undo: id = " + u.getUndoId() +
			// " key =  " + u.getUndoKey() + " class = " +
			// u.getUndoObject().getClass().getSimpleName() + " len = " +
			// UndoListLength);

			if (u.isWrite() == 1) {// undo write
				// this.tempUndoKey = this.UndoList[0].getUndoKey();
				if (this.remove(u.getUndoObject(), u.getUndoKey(), true)) {
					this.removeUndo();
					// System.out.println("undo object " +
					// u.getUndoObject().getId() + " removed sucessfully");
				} else
					System.out.println("undo NOT removed sucessfully");
			}
			if (u.isWrite() == 0) {// undo a remove
				// this.tempUndoKey = this.UndoList[0].getUndoKey();
				if (this.write(u.getUndoObject(), u.getUndoKey(),
						u.getUndoClass(), true)) {
					this.removeUndo();
					// System.out.println("undo object added sucessfully");
				} else {
					System.out
							.println("undo adding NOTNOTNOTNOTNOT completed sucessfully");

				}

			}
			if (u.isWrite() == 2) {// undo an update

				if (this.update(u.getUndoId(), u.getUndoObject(),
						u.getUndoKey(), u.getUndoClass(), true)) {
					// System.out.println("Undid update: " + u.getId());
					this.removeUndo();
				} else
					System.out.println("undoing update failed miserably");
			}
		}
	}

}
